﻿using System;
using System.IO;

namespace SmartCard.Framework
{
	/// <summary>
	/// Application Protocol Data Unit as defined in ISO-7816-4
	/// edition 2.0
	/// </summary>
	public class ApduCommand
	{
		#region fields

		private const uint _MinimumApduLength = 4;

		#endregion

		#region properties

		/// <summary>
		/// Get the instruction class
		/// </summary>
		public byte InstructionClass
		{ get; private set; }

		/// <summary>
		/// Get the instruction code
		/// </summary>
		public byte InstructionCode
		{ get; private set; }

		/// <summary>
		/// Get param 1
		/// </summary>
		public byte Param1
		{ get; private set; }

		/// <summary>
		/// Get Param 2
		/// </summary>
		public byte Param2
		{ get; private set; }

		/// <summary>
		/// Get Param 3
		/// </summary>
		public ushort Param3
		{ get; private set; }

		/// <summary>
		/// Command data, with a max length up to 65,535
		/// bytes of data
		/// </summary>
		public byte[] CommandData
		{ get; private set; }

		/// <summary>
		/// Max response length with a max value of
		/// 65,535
		/// </summary>
		public ushort MaxResponseLength
		{ get; private set; }

		#endregion

		#region construction

		/// <summary>
		/// Construct an APDU request that has no command data, and leverages param3 as a part
		/// of the instruction.
		/// </summary>
		/// <param name="instructionClass"></param>
		/// <param name="instructionCode"></param>
		/// <param name="param1"></param>
		/// <param name="param2"></param>
		/// <param name="param3"></param>
		/// <param name="maxResponseLength"></param>
		public ApduCommand(
			byte instructionClass, byte instructionCode,
			byte param1, byte param2, ushort param3,
			ushort maxResponseLength)
			: this(instructionClass, instructionCode, param1, param2, param3, null, maxResponseLength)
		{ }

		/// <summary>
		/// Create an APDU request that sends the specified command data.
		/// </summary>
		/// <param name="instructionClass"></param>
		/// <param name="instructionCode"></param>
		/// <param name="param1"></param>
		/// <param name="param2"></param>
		/// <param name="commandData"></param>
		/// <param name="maxResponseLength"></param>
		public ApduCommand(
			byte instructionClass, byte instructionCode,
			byte param1, byte param2, byte[] commandData, ushort maxResponseLength)
			: this(instructionClass, instructionCode, param1, param2,
					((commandData == null) ? (ushort)0 : (ushort)commandData.Length),
					commandData, maxResponseLength)
		{ }

		protected ApduCommand(
			byte instructionClass, byte instructionCode,
			byte param1, byte param2, UInt16 param3,
			byte[] commandData, ushort maxResponseLength)
		{
			if (commandData != null)
			{
				if (commandData.Length > ushort.MaxValue)
				{
					throw new ArgumentException(
					  string.Format("array length must be less than 0xFFFF in size.  It was [{0}]", commandData.Length),
					  "commandData");
				}
			}

			InstructionClass = instructionClass;
			InstructionCode = instructionCode;
			Param1 = param1;
			Param2 = param2;
			Param3 = param3;
			CommandData = commandData;
			MaxResponseLength = maxResponseLength;
		}

		#endregion

		#region public helpers

		/// <summary>
		/// Get the raw apdu message
		/// </summary>
		public byte[] GetRaw(bool excludeResultLength)
		{
			// Convert to the bytes in the correct big endien order for
			// the max response length.
			byte[] maxLengthTempBytes = BitConverter.GetBytes(MaxResponseLength);
			if (BitConverter.IsLittleEndian)
			{ Array.Reverse(maxLengthTempBytes); }

			// Convert to the bytes in the correct big endien order for
			// the max command data length
			byte[] commandDataTempBytes = null;
			if (Param3 > 0)
			{
				commandDataTempBytes = BitConverter.GetBytes((ushort)Param3);
				if (BitConverter.IsLittleEndian)
				{ Array.Reverse(commandDataTempBytes); }
			}

			// default to simplify addition logic later, no need to check
			// for nulls
			byte[] commandDataLengthBytes = new byte[0];
			byte[] maxResponseLengthBytes = new byte[0];

			// we must determine if either the command length or the
			// response length are greater than 255, if they are, then
			// both values must be specified by 3 bytes.  a 0x00 followed
			// by two bytes from 0x0001 to 0xFFFF (1 - 65,535); a short value
			// in big-endien format
			if (MaxResponseLength > 0xFF ||
				(Param3 > 0xFF))
			{
				if (MaxResponseLength > 0)
				{
					maxResponseLengthBytes = new byte[3];
					Array.Copy(maxLengthTempBytes, 0, maxResponseLengthBytes, 1, 2);
				}

				if (Param3 > 0)
				{
					commandDataLengthBytes = new byte[3];
					Array.Copy(commandDataTempBytes, 0, commandDataLengthBytes, 1, 2);
				}
			}
			else
			{
				if (MaxResponseLength > 0)
				{
					maxResponseLengthBytes = new byte[1];
					maxResponseLengthBytes[0] = maxLengthTempBytes[1];
				}

				if (Param3 > 0)
				{
					commandDataLengthBytes = new byte[1];
					commandDataLengthBytes[0] = commandDataTempBytes[1];
				}
			}
			
			// just write into a memory stream to create our APDU
			byte[] raw = null;
			using (var ms = new MemoryStream())
			{
				ms.WriteByte(InstructionClass);
				ms.WriteByte(InstructionCode);
				ms.WriteByte(Param1);
				ms.WriteByte(Param2);

				if (commandDataLengthBytes != null)
				{ ms.Write(commandDataLengthBytes, 0, commandDataLengthBytes.Length); }

				if (CommandData != null)
				{ ms.Write(CommandData, 0, CommandData.Length); }

				if (!excludeResultLength && 
					maxResponseLengthBytes != null)
				{ ms.Write(maxResponseLengthBytes, 0, maxResponseLengthBytes.Length); }

				raw = ms.ToArray();
			}

			return raw;
		}

		#endregion
	}
}
